iBetter Books
수정

PART 07의 마무리는 지금까지의 조각을 모은 1:N 대용량 얼굴 검색 시스템입니다. InsightFace로 임베딩을 뽑고(2장), faiss로 색인해(4장), 질의 얼굴과 가장 닮은 사람을 찾습니다. PART 06의 find가 수천 명용이었다면, 이 시스템은 수십만 명까지 확장됩니다.

시스템의 두 단계

얼굴 검색 시스템은 등록(인덱스 구축)과 검색의 두 단계로 나뉩니다. 등록은 한 번 또는 가끔, 검색은 자주 일어납니다.

flowchart TB subgraph REG[등록 단계] direction TB A[갤러리 사진들] --> B[InsightFace 임베딩] B --> C[faiss 인덱스 구축] C --> D[인덱스·이름 저장] end subgraph SCH[검색 단계] direction TB E[질의 얼굴] --> F[InsightFace 임베딩] F --> G[faiss 검색 top-k] G --> H[이름·유사도 반환] end REG --> SCH

1단계 — 갤러리 인덱스 구축

인물별 폴더의 사진에서 임베딩을 모아 faiss 인덱스를 만들고 저장합니다.

# 파일: build_index.py"""갤러리에서 InsightFace 임베딩을 모아 faiss 인덱스를 만든다."""import osimport globimport pickleimport numpy as npimport cv2import faissfrom insightface.app import FaceAnalysisapp = FaceAnalysis(name="buffalo_l", providers=["CPUExecutionProvider"])app.prepare(ctx_id=-1, det_size=(640, 640))embeddings, names = [], []for person in sorted(os.listdir("gallery")):    for path in glob.glob(os.path.join("gallery", person, "*.jpg")):        faces = app.get(cv2.imread(path))        if not faces:            continue        embeddings.append(faces[0].normed_embedding)   # 정규화 임베딩        names.append(person)X = np.array(embeddings, dtype="float32")faiss.normalize_L2(X)                       # 안전하게 한 번 더 정규화index = faiss.IndexFlatIP(512)index.add(X)faiss.write_index(index, "faces.index")     # 인덱스 저장with open("names.pkl", "wb") as f:          # 같은 순서의 이름 저장    pickle.dump(names, f)print(f"등록 완료: {len(names)}건, 인물 {len(set(names))}명")

normed_embedding을 모아 IndexFlatIP에 넣고, 인덱스와 이름을 파일로 저장합니다. 이름은 임베딩과 같은 순서로 저장하는 것이 핵심입니다. 검색 결과가 위치 번호로 나오므로, 그 번호로 이름을 찾기 때문입니다.

2단계 — 질의 검색

저장한 인덱스를 불러와 질의 얼굴을 검색합니다.

# 파일: search_query.py"""저장한 인덱스로 질의 얼굴의 신원을 찾는다."""import pickleimport numpy as npimport cv2import faissfrom insightface.app import FaceAnalysisapp = FaceAnalysis(name="buffalo_l", providers=["CPUExecutionProvider"])app.prepare(ctx_id=-1, det_size=(640, 640))index = faiss.read_index("faces.index")with open("names.pkl", "rb") as f:    names = pickle.load(f)faces = app.get(cv2.imread("query.jpg"))for face in faces:    q = np.array([face.normed_embedding], dtype="float32")    faiss.normalize_L2(q)    D, I = index.search(q, k=3)              # top-3 후보    best_sim, best_idx = float(D[0][0]), int(I[0][0])    if best_sim > 0.4:                       # 코사인 임계값(데이터로 조정)        print(f"식별: {names[best_idx]} (유사도 {best_sim:.3f})")    else:        print(f"미등록자 (최고 유사도 {best_sim:.3f})")

질의 임베딩을 정규화해 search하면 가장 닮은 후보들이 나옵니다. 첫 후보의 유사도가 임계값(0.4)을 넘으면 그 이름으로 식별하고, 못 넘으면 미등록자로 처리합니다. PART 04의 1:N 식별과 임계값 판정이 그대로, 그러나 대규모에서도 빠르게 동작합니다.

실전을 위한 보강

이 시스템을 실제 운영하려면 몇 가지를 더합니다.

  • 여러 장 등록: 인물당 여러 사진의 임베딩을 모두 넣으면 다양한 각도에 강해집니다(PART 05 DB와 같은 원리).
  • 품질 필터: det_score가 낮은 흐릿한 얼굴은 등록·검색에서 제외해 노이즈를 줄입니다.
  • 위조 방지: 사진·화면을 들이대는 부정 인증을 막으려면 라이브니스 검사가 필요합니다(PART 09).
  • 개인정보: 임베딩과 이름은 민감 정보이므로 접근 통제·보관 기간을 명확히 합니다(PART 11).

실무 팁. 대규모 검색 시스템에서 가장 흔한 실수는 "임베딩과 이름의 순서가 어긋나는 것"입니다. 등록 중 얼굴을 못 찾아 건너뛴 사진이 있으면, 임베딩은 빠졌는데 이름은 그대로여서 번호가 밀립니다. 위 코드처럼 임베딩을 추가할 때만 이름도 함께 추가하면(둘을 같은 루프에서 append) 이 어긋남을 원천 차단할 수 있습니다.

이 장에서 기억할 것

1:N 대용량 검색 시스템은 등록(InsightFace 임베딩 → faiss 인덱스 저장)과 검색(질의 임베딩 → top-k → 임계값 판정)의 두 단계로 만듭니다. normed_embeddingIndexFlatIP로 코사인 검색을 하고, 임베딩과 이름을 같은 순서로 관리하는 것이 핵심입니다. 실전화에는 다중 등록·품질 필터·위조 방지·개인정보 보호가 더해집니다. 이로써 PART 07이 끝납니다. 다음 PART 08에서는 신원을 넘어 표정과 얼굴 속성, 곧 감정 인식으로 들어갑니다.

05. [실습] 1대N 대용량 얼굴 검색 시스템 — 깊이 파는 얼굴 인식과 감정 분석 | iBetter Books